# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Projects::VulnerabilityFeedbackController, feature_category: :vulnerability_management do
  let_it_be(:group)   { create(:group) }
  let_it_be(:project) { create(:project, :public, :repository, namespace: group) }
  let_it_be(:user)    { create(:user) }
  let_it_be(:guest)   { create(:user) }

  before do
    group.add_developer(user)
  end

  describe 'GET #count' do
    let_it_be(:pipeline_1) { create(:ci_pipeline, project: project) }
    let_it_be(:pipeline_2) { create(:ci_pipeline, project: project) }

    let_it_be(:merge_request) { create(:merge_request, source_project: project) }

    let_it_be(:vuln_feedback_1) { create(:vulnerability_feedback, :dismissal, :sast, project: project, author: user, pipeline: pipeline_1) }
    let_it_be(:vuln_feedback_2) { create(:vulnerability_feedback, :issue, :sast, project: project, author: user, pipeline: pipeline_1) }
    let_it_be(:vuln_feedback_3) { create(:vulnerability_feedback, :dismissal, :sast, project: project, author: user, pipeline: pipeline_2) }
    let_it_be(:vuln_feedback_4) { create(:vulnerability_feedback, :dismissal, :dependency_scanning, project: project, author: user, pipeline: pipeline_2) }
    let_it_be(:vuln_feedback_5) { create(:vulnerability_feedback, :merge_request, :dependency_scanning, project: project, author: user, pipeline: pipeline_1, merge_request: merge_request) }

    context '@vulnerability_feedback' do
      before do
        sign_in(user)
      end

      it 'returns a successful 200 response' do
        count_feedbacks

        expect(response).to have_gitlab_http_status(:ok)
      end

      it 'returns project feedbacks list' do
        count_feedbacks

        expect(json_response).to eq("count" => 5)
      end

      context 'with filter params' do
        it 'returns project feedbacks list filtered on category' do
          count_feedbacks({ category: 'sast' })

          expect(json_response).to eq("count" => 3)
        end

        it 'returns project feedbacks list filtered on feedback_type' do
          count_feedbacks({ feedback_type: 'issue' })

          expect(json_response).to eq("count" => 1)
        end

        it 'returns project feedbacks list filtered on category and feedback_type' do
          count_feedbacks({ category: 'sast', feedback_type: 'dismissal' })

          expect(json_response).to eq("count" => 2)
        end
      end
    end

    def count_feedbacks(params = {})
      get :count, params: { namespace_id: project.namespace.to_param, project_id: project }.merge(params)
    end
  end

  describe 'GET #index' do
    let_it_be(:pipeline_1) { create(:ci_pipeline, project: project) }
    let_it_be(:pipeline_2) { create(:ci_pipeline, project: project) }

    let_it_be(:merge_request) { create(:merge_request, source_project: project) }

    let_it_be(:vuln_feedback_1) { create(:vulnerability_feedback, :dismissal, :sast, project: project, author: user, pipeline: pipeline_1) }
    let_it_be(:vuln_feedback_2) { create(:vulnerability_feedback, :issue, :sast, project: project, author: user, pipeline: pipeline_1) }
    let_it_be(:vuln_feedback_3) { create(:vulnerability_feedback, :dismissal, :sast, project: project, author: user, pipeline: pipeline_2) }
    let_it_be(:vuln_feedback_4) { create(:vulnerability_feedback, :dismissal, :dependency_scanning, project: project, author: user, pipeline: pipeline_2) }
    let_it_be(:vuln_feedback_5) { create(:vulnerability_feedback, :merge_request, :dependency_scanning, project: project, author: user, pipeline: pipeline_1, merge_request: merge_request) }

    context '@vulnerability_feedback' do
      before do
        sign_in(user)
      end

      context 'when pagination parameters are given' do
        let_it_be(:page) { 0 }
        let_it_be(:per_page) { 1 }

        before do
          list_feedbacks(page: page, per_page: per_page)
        end

        context 'when page and per page are given' do
          it 'returns the desired quantity of vulnerability_feedbacks' do
            expect(json_response.length).to eq per_page
          end
        end

        context 'when a following page is requested' do
          let_it_be(:page) { 2 }
          let_it_be(:per_page) { 1 }

          it 'returns the expected vulnerability_feedbacks per pagination' do
            expect(json_response.first['id']).to eq(vuln_feedback_2.id)
          end
        end

        context 'when just page is given' do
          let_it_be(:per_page) { nil }

          it 'returns the default quantity of vulnerability_feedbacks' do
            expect(json_response.length).to eq 5
          end
        end

        context 'when just per_page is given' do
          let_it_be(:page) { nil }

          it 'returns the desired quantity of vulnerability_feedbacks' do
            expect(json_response.length).to eq per_page
          end

          it 'returns the first page of vulnerability_feedbacks' do
            expect(json_response.first['id']).to eq(vuln_feedback_1.id)
          end
        end

        context 'when an invalid page is given' do
          let_it_be(:page) { "abc" }

          it 'returns the desired quantity of vulnerability_feedbacks' do
            expect(json_response.length).to eq per_page
          end

          it 'returns the first page of vulnerability_feedbacks' do
            expect(json_response.first['id']).to eq(vuln_feedback_1.id)
          end
        end

        context 'when an invalid per_page is given' do
          let_it_be(:per_page) { "abc" }

          it 'returns the default quantity of vulnerability_feedbacks' do
            expect(json_response.length).to eq 5
          end

          it 'returns the first page of vulnerability_feedbacks' do
            expect(json_response.first['id']).to eq(vuln_feedback_1.id)
          end
        end
      end

      it 'returns a successful 200 response' do
        list_feedbacks

        expect(response).to have_gitlab_http_status(:ok)
      end

      it 'returns project feedbacks list' do
        list_feedbacks

        expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
        expect(json_response.length).to eq 5
      end

      # TODO: Remove once bad/invalid data has been deleted https://gitlab.com/gitlab-org/gitlab/-/issues/218837
      context 'when the pipeline has been set to another project' do
        let!(:vuln_feedback_in_other_proj) do
          feedback = build(
            :vulnerability_feedback,
            project: project,
            author: user,
            pipeline: create(:ci_pipeline)
          )

          # Simulating vulnerable data that was in the DB before we introduced
          # the validation preventing
          feedback.save!(validate: false)

          feedback
        end

        it 'does not present the pipeline' do
          list_feedbacks

          expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
          expect(json_response.length).to eq 6
          feedback_with_invalid_pipeline_response = json_response.find { |r| r['id'] == vuln_feedback_in_other_proj.id }
          expect(feedback_with_invalid_pipeline_response['pipeline']).to be_nil
        end
      end

      context 'with filter params' do
        it 'returns project feedbacks list filtered on category' do
          list_feedbacks({ category: 'sast' })

          expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
          expect(json_response.length).to eq 3
        end

        it 'returns project feedbacks list filtered on feedback_type' do
          list_feedbacks({ feedback_type: 'issue' })

          expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
          expect(json_response.length).to eq 1
        end

        it 'returns project feedbacks list filtered on category and feedback_type' do
          list_feedbacks({ category: 'sast', feedback_type: 'dismissal' })

          expect(response).to match_response_schema('vulnerability_feedback_list', dir: 'ee')
          expect(json_response.length).to eq 2
        end
      end

      context 'with unauthorized user for given project' do
        let(:unauthorized_user) { create(:user) }
        let(:project) { create(:project, :private, namespace: group) }

        before do
          sign_in(unauthorized_user)
        end

        it 'returns a 404 response' do
          list_feedbacks

          expect(response).to have_gitlab_http_status(:not_found)
        end
      end
    end

    def list_feedbacks(params = {})
      get :index, params: { namespace_id: project.namespace.to_param, project_id: project }.merge(params)
    end
  end

  describe 'POST #create' do
    let(:pipeline) { create(:ci_pipeline, project: project) }
    let(:create_params) do
      {
        feedback_type: 'dismissal', pipeline_id: pipeline.id, category: 'sast',
        comment: 'a dismissal comment',
        project_fingerprint: '418291a26024a1445b23fe64de9380cdcdfd1fa8',
        vulnerability_data: {
          category: 'sast',
          severity: 'Low',
          confidence: 'Medium',
          cve: '818bf5dacb291e15d9e6dc3c5ac32178:PREDICTABLE_RANDOM',
          title: 'Predictable pseudorandom number generator',
          description: 'Description of Predictable pseudorandom number generator',
          tool: 'find_sec_bugs',
          location: {
            file: 'subdir/src/main/java/com/gitlab/security_products/tests/App.java',
            start_line: '41',
            blob_path: '/group_path/project_path/-/blob/commitsha/subdir/src/main/App.java#L15'
          },
          identifiers: [{
            type: 'CVE',
            name: 'CVE-2018-1234',
            value: 'CVE-2018-1234',
            url: 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-1234'
          }],
          links: [{
            name: 'Awesome-security blog post',
            url: 'https;//example.com/blog-post'
          }],
          scanner: {
            external_id: 'gemnasium',
            name: 'Gemnasium',
            vendor: 'Gemnasium'
          },
          scan: {
            type: 'dependency_scanning',
            status: 'success',
            start_time: 'placeholder',
            end_time: 'placeholder'
          }
        }
      }
    end

    before do
      stub_licensed_features(security_dashboard: true)
    end

    shared_examples 'vulnerability response' do
      let_it_be(:vulnerability) { create(:vulnerability) }
      let_it_be(:finding) { create(:vulnerabilities_finding, vulnerability: vulnerability) }
      let_it_be(:vuln_feedback) { create(:vulnerability_feedback, :dismissal, finding_uuid: finding.uuid) }

      before do
        allow_next_instance_of(VulnerabilityFeedback::CreateService) do |instance|
          allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { vulnerability_feedback: vuln_feedback }))
        end
      end

      it 'renders vulnerability serializer with vulnerability object' do
        expect_next_instance_of(VulnerabilitySerializer) do |instance|
          expect(instance).to receive(:represent).with(vulnerability)
        end

        create_feedback user: user, project: project, params: create_params
      end
    end

    context 'with valid params' do
      it_behaves_like 'vulnerability response'

      context 'when id of a vulnerability is not provided' do
        subject { create_feedback user: user, project: project, params: create_params.deep_merge(feedback_type: 'issue', vulnerability_data: { vulnerability_id: nil }) }

        it 'creates no vulnerability issue link for related vulnerability' do
          expect { subject }.not_to change { Vulnerabilities::IssueLink.count }
        end
      end

      context 'when id of a vulnerability is provided' do
        let!(:vulnerability) { create(:vulnerability, :with_findings, project: project) }

        subject { create_feedback user: user, project: project, params: create_params.deep_merge(feedback_type: 'issue', vulnerability_data: { vulnerability_id: vulnerability.id }) }

        it 'creates vulnerability issue link for related vulnerability' do
          expect { subject }.to change { Vulnerabilities::IssueLink.count }.by(1)
        end
      end
    end

    context 'with invalid params' do
      it 'returns a forbidden 403 response when feedbback_type is nil' do
        create_feedback user: user, project: project, params: create_params.except(:feedback_type)

        expect(response).to have_gitlab_http_status(:forbidden)
      end

      it 'returns a forbidden 403 response when feedbback_type is invalid' do
        create_feedback user: user, project: project, params: create_params.merge(feedback_type: 'foo')

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'with unauthorized user for feedback creation' do
      context 'for issue feedback' do
        it 'returns a forbidden 403 response' do
          group.add_guest(guest)

          create_feedback user: guest, project: project, params: create_params.merge(feedback_type: 'issue')

          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end

      context 'for merge_request feedback' do
        it 'returns a forbidden 403 response' do
          group.add_guest(guest)

          create_feedback user: guest, project: project, params: create_params.merge(feedback_type: 'merge_request')

          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end

      context 'for dismissal feedback' do
        it 'returns a forbidden 403 response' do
          group.add_guest(guest)

          create_feedback user: guest, project: project, params: create_params.merge(feedback_type: 'dismissal')

          expect(response).to have_gitlab_http_status(:forbidden)
        end
      end
    end

    context 'with unauthorized user for given project' do
      let(:unauthorized_user) { create(:user) }
      let(:project) { create(:project, :private, namespace: group) }

      it 'returns a 404 response' do
        create_feedback user: unauthorized_user, project: project, params: create_params

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    context 'with pipeline in another project' do
      let(:pipeline) { create(:ci_pipeline) }

      it 'returns a 422 response' do
        create_feedback user: user, project: project, params: create_params

        expect(response).to have_gitlab_http_status(:unprocessable_entity)
        expect(json_response).to eq({ "pipeline" => ["must associate the same project"] })
      end
    end

    context 'with nonexistent pipeline_id' do
      let(:pipeline) { build(:ci_pipeline, id: -10) }

      it 'returns a 422 response' do
        create_feedback user: user, project: project, params: create_params

        expect(response).to have_gitlab_http_status(:unprocessable_entity)
        expect(json_response).to eq({ "pipeline" => ["must associate the same project"] })
      end
    end

    context 'with nil pipeline_id' do
      let(:pipeline) { build(:ci_pipeline, id: nil) }

      it_behaves_like 'vulnerability response'
    end

    def create_feedback(user:, project:, params:)
      sign_in(user)
      post_params = {
        namespace_id: project.namespace.to_param,
        project_id: project,
        vulnerability_feedback: params,
        format: :json
      }

      post :create, params: post_params, as: :json
    end
  end

  describe 'PATCH #update' do
    let(:vuln_feedback) do
      create(
        :vulnerability_feedback, :dismissal, :sast, :comment,
        project: project,
        author: user
      )
    end

    context 'with valid params' do
      let_it_be(:vulnerability) { create(:vulnerability) }
      let_it_be(:finding) { create(:vulnerabilities_finding, vulnerability: vulnerability) }
      let_it_be(:feedback) { create(:vulnerability_feedback, :dismissal, project: project, finding_uuid: finding.uuid) }

      before do
        vuln_feedback.comment = 'new dismissal comment'
        update_feedback user: user, params: vuln_feedback
        allow_next_instance_of(VulnerabilityFeedbackModule::UpdateService) do |instance|
          allow(instance).to receive(:execute).and_return(ServiceResponse.success(payload: { vulnerability_feedback: feedback }))
        end
      end

      it 'renders vulnerability serializer with vulnerability object' do
        expect_next_instance_of(VulnerabilitySerializer) do |instance|
          expect(instance).to receive(:represent).with(vulnerability)
        end

        update_feedback user: user, params: feedback
      end

      it 'returns a successful 200 response' do
        expect(response).to have_gitlab_http_status(:ok)
      end

      it 'updates the comment attributes' do
        expect(vuln_feedback.reload.comment).to eq('new dismissal comment')
      end
    end

    context 'with invalid params' do
      it 'returns a not found 404 response for invalid vulnerability feedback id' do
        vuln_feedback.id = 123
        update_feedback user: user, params: vuln_feedback

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    context 'with unauthorized user for feedback update' do
      it 'returns a forbidden 403 response' do
        group.add_guest(guest)

        update_feedback user: guest, params: vuln_feedback

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'with unauthorized user for given project' do
      let(:unauthorized_user) { create(:user) }
      let(:project) { create(:project, :private, namespace: group) }

      it 'returns a 404 response' do
        update_feedback user: unauthorized_user, params: vuln_feedback

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    def update_feedback(user:, params:)
      sign_in(user)

      patch :update, params: {
                       namespace_id: project.namespace.to_param,
                       project_id: project,
                       id: params[:id],
                       vulnerability_feedback: params
                     }, as: :json
    end
  end

  describe 'DELETE #destroy' do
    let(:pipeline) { create(:ci_pipeline, project: project) }
    let!(:vuln_feedback) { create(:vulnerability_feedback, :dismissal, :sast, project: project, author: user, pipeline: pipeline) }

    context 'with valid params' do
      it 'returns a successful 204 response' do
        destroy_feedback user: user, project: project, id: vuln_feedback.id

        expect(response).to have_gitlab_http_status(:no_content)
      end
    end

    context 'with invalid params' do
      it 'returns a not found 404 response for invalid vulnerability feedback id' do
        destroy_feedback user: user, project: project, id: 123

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    context 'with unauthorized user for feedback deletion' do
      it 'returns a forbidden 403 response' do
        group.add_guest(guest)

        destroy_feedback user: guest, project: project, id: vuln_feedback.id

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'with unauthorized user for given project' do
      let(:unauthorized_user) { create(:user) }
      let(:project) { create(:project, :private, namespace: group) }

      it 'returns a 404 response' do
        destroy_feedback user: unauthorized_user, project: project, id: vuln_feedback.id

        expect(response).to have_gitlab_http_status(:not_found)
      end
    end

    context 'for issue feedback' do
      let!(:vuln_feedback) { create(:vulnerability_feedback, :issue, :sast, project: project, author: user, pipeline: pipeline) }

      it 'returns a forbidden 403 response' do
        destroy_feedback user: user, project: project, id: vuln_feedback.id

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    context 'for merge_request feedback' do
      let!(:vuln_feedback) { create(:vulnerability_feedback, :merge_request, :sast, project: project, author: user, pipeline: pipeline) }

      it 'returns a forbidden 403 response' do
        destroy_feedback user: user, project: project, id: vuln_feedback.id

        expect(response).to have_gitlab_http_status(:forbidden)
      end
    end

    def destroy_feedback(user:, project:, id:)
      sign_in(user)

      delete :destroy, params: { namespace_id: project.namespace.to_param, project_id: project, id: id }
    end
  end
end
